﻿using ImageK;
using System;
using System.Collections.Generic;
using System.IO.Packaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ImageK.Java.Extensions;

namespace ImageK.IO
{
    /**Saves an image described by a FileInfo object as an uncompressed TIFF file.*/
    public class TiffEncoder
    {
        static int HDR_SIZE = 8;
        static int MAP_SIZE = 768; // in 16-bit words
        static int BPS_DATA_SIZE = 6;
        static int SCALE_DATA_SIZE = 16;

        private FileInfo fi;
        private int bitsPerSample;
        private int photoInterp;
        private int samplesPerPixel;
        private int nEntries;
        private int ifdSize;
        private long imageOffset;
        private int imageSize;
        private long stackSize;
        private byte[] description;
        private int metaDataSize;
        private int nMetaDataTypes;
        private int nMetaDataEntries;
        private int nSliceLabels;
        private int extraMetaDataEntries;
        private int scaleSize;
        private bool littleEndian = Prefs.intelByteOrder;
        private byte[] buffer = new byte[8];
        private int colorMapSize = 0;


        public TiffEncoder(FileInfo fi)
        {
            this.fi = fi;
            fi.intelByteOrder = littleEndian;
            bitsPerSample = 8;
            samplesPerPixel = 1;
            nEntries = 10;
            int bytesPerPixel = 1;
            int bpsSize = 0;

            switch (fi.fileType)
            {
                case FileInfo.GRAY8:
                    photoInterp = fi.whiteIsZero ? 0 : 1;
                    break;
                case FileInfo.GRAY16_UNSIGNED:
                case FileInfo.GRAY16_SIGNED:
                    bitsPerSample = 16;
                    photoInterp = fi.whiteIsZero ? 0 : 1;
                    if (fi.lutSize>0)
                    {
                        nEntries++;
                        colorMapSize = MAP_SIZE*2;
                    }
                    bytesPerPixel = 2;
                    break;
                case FileInfo.GRAY32_FLOAT:
                    bitsPerSample = 32;
                    photoInterp = fi.whiteIsZero ? 0 : 1;
                    if (fi.lutSize>0)
                    {
                        nEntries++;
                        colorMapSize = MAP_SIZE*2;
                    }
                    bytesPerPixel = 4;
                    break;
                case FileInfo.RGB:
                    photoInterp = 2;
                    samplesPerPixel = 3;
                    bytesPerPixel = 3;
                    bpsSize = BPS_DATA_SIZE;
                    break;
                case FileInfo.RGB48:
                    bitsPerSample = 16;
                    photoInterp = 2;
                    samplesPerPixel = 3;
                    bytesPerPixel = 6;
                    fi.nImages /= 3;
                    bpsSize = BPS_DATA_SIZE;
                    break;
                case FileInfo.COLOR8:
                    photoInterp = 3;
                    nEntries++;
                    colorMapSize = MAP_SIZE*2;
                    break;
                default:
                    photoInterp = 0;
                    break;
            }
            if (fi.unit!=null && fi.pixelWidth!=0 && fi.pixelHeight!=0)
                nEntries += 3; // XResolution, YResolution and ResolutionUnit
            if (fi.fileType==FileInfo.GRAY32_FLOAT)
                nEntries++; // SampleFormat tag
            makeDescriptionString();
            if (description!=null)
                nEntries++;  // ImageDescription tag
            long size = (long)fi.width*fi.height*bytesPerPixel;
            imageSize = size<=0xffffffffL ? (int)size : 0;
            stackSize = (long)imageSize*fi.nImages;
            metaDataSize = getMetaDataSize();
            if (metaDataSize>0)
                nEntries += 2; // MetaData & MetaDataCounts
            ifdSize = 2 + nEntries*12 + 4;
            int descriptionSize = description!=null ? description.Length : 0;
            scaleSize = fi.unit!=null && fi.pixelWidth!=0 && fi.pixelHeight!=0 ? SCALE_DATA_SIZE : 0;
            imageOffset = HDR_SIZE+ifdSize+bpsSize+descriptionSize+scaleSize+colorMapSize + nMetaDataEntries*4 + metaDataSize;
            fi.offset = (int)imageOffset;
            //ij.IJ.log(imageOffset+", "+ifdSize+", "+bpsSize+", "+descriptionSize+", "+scaleSize+", "+colorMapSize+", "+nMetaDataEntries*4+", "+metaDataSize);
        }

        /** Saves the image as a TIFF file. The OutputStream is not closed.
            The fi.pixels field must contain the image data. If fi.nImages>1
            then fi.pixels must be a 2D array. The fi.offset field is ignored. */
        public void write(BinaryWriter ot)
        {
            writeHeader(ot);
		    long nextIFD = 0L;
		    if (fi.nImages>1)
			    nextIFD = imageOffset+stackSize;
		    bool bigTiff = nextIFD+fi.nImages*ifdSize>=0xffffffffL;
		    if (bigTiff)
                nextIFD = 0L;
            writeIFD(ot, (int) imageOffset, (int) nextIFD);
		    if (fi.fileType==FileInfo.RGB||fi.fileType==FileInfo.RGB48)
			    writeBitsPerPixel(ot);
		    if (description!=null)
			    writeDescription(ot);
		    if (scaleSize>0)
			    writeScale(ot);
		    if (colorMapSize>0)
			    writeColorMap(ot);
		    if (metaDataSize>0)
			    writeMetaData(ot);
            new ImageWriter(fi).write(ot);
		    if (nextIFD>0L) {
			    int ifdSize2 = ifdSize;
			    if (metaDataSize>0) {
				    metaDataSize = 0;
				    nEntries -= 2;
				    ifdSize2 -= 2*12;
			    }
			    for (int i=2; i<=fi.nImages; i++) {
				    if (i==fi.nImages)
					    nextIFD = 0;
				    else
					    nextIFD += ifdSize2;
				    imageOffset += imageSize;
				    writeIFD(ot, (int) imageOffset, (int) nextIFD);
                }
		    } else if (bigTiff)
                IJ.log("Stack is larger than 4GB. Most TIFF readers will only open the first image. Use this information to open as raw:\n"+fi);
	    }
	
	    // public void write(BinaryWriter ot)
     //    {
     //        write((BinaryWriter)ot);
     //    }

        int getMetaDataSize()
        {
            nSliceLabels = 0;
            nMetaDataEntries = 0;
            int size = 0;
            int nTypes = 0;
            if (fi.info!=null && fi.info.Length>0)
            {
                nMetaDataEntries = 1;
                size = fi.info.Length*2;
                nTypes++;
            }
            if (fi.sliceLabels!=null)
            {
                int max = Math.Min(fi.sliceLabels.Length, fi.nImages);
                bool isNonNullLabel = false;
                for (int i = 0; i<max; i++)
                {
                    if (fi.sliceLabels[i]!=null && fi.sliceLabels[i].Length>0)
                    {
                        isNonNullLabel = true;
                        break;
                    }
                }
                if (isNonNullLabel)
                {
                    for (int i = 0; i<max; i++)
                    {
                        nSliceLabels++;
                        if (fi.sliceLabels[i]!=null)
                            size += fi.sliceLabels[i].Length*2;
                    }
                    if (nSliceLabels>0) nTypes++;
                    nMetaDataEntries += nSliceLabels;
                }
            }

            if (fi.displayRanges!=null)
            {
                nMetaDataEntries++;
                size += fi.displayRanges.Length*8;
                nTypes++;
            }

            if (fi.channelLuts!=null)
            {
                for (int i = 0; i<fi.channelLuts.Length; i++)
                {
                    if (fi.channelLuts[i]!=null)
                        size += fi.channelLuts[i].Length;
                }
                nTypes++;
                nMetaDataEntries += fi.channelLuts.Length;
            }

            if (fi.plot!=null)
            {
                nMetaDataEntries++;
                size += fi.plot.Length;
                nTypes++;
            }

            if (fi.roi!=null)
            {
                nMetaDataEntries++;
                size += fi.roi.Length;
                nTypes++;
            }

            if (fi.overlay!=null)
            {
                for (int i = 0; i<fi.overlay.Length; i++)
                {
                    if (fi.overlay[i]!=null)
                        size += fi.overlay[i].Length;
                }
                nTypes++;
                nMetaDataEntries += fi.overlay.Length;
            }

            if (fi.properties!=null)
            {
                for (int i = 0; i<fi.properties.Length; i++)
                    size += fi.properties[i].Length*2;
                nTypes++;
                nMetaDataEntries += fi.properties.Length;
            }

            if (fi.metaDataTypes!=null && fi.metaData!=null && fi.metaData[0]!=null
            && fi.metaDataTypes.Length==fi.metaData.Length)
            {
                extraMetaDataEntries = fi.metaData.Length;
                nTypes += extraMetaDataEntries;
                nMetaDataEntries += extraMetaDataEntries;
                for (int i = 0; i<extraMetaDataEntries; i++)
                {
                    if (fi.metaData[i]!=null)
                        size += fi.metaData[i].Length;
                }
            }
            if (nMetaDataEntries>0) nMetaDataEntries++; // add entry for header
            int hdrSize = 4 + nTypes*8;
            if (size>0) size += hdrSize;
            nMetaDataTypes = nTypes;
            return size;
        }

        /** Writes the 8-byte image file header. */
        void writeHeader(BinaryWriter ot)
        {
		    byte[]
            hdr = new byte[8];
            if (littleEndian)
            {
                hdr[0] = 73; // "II" (Intel byte order)
                hdr[1] = 73;
                hdr[2] = 42;  // 42 (magic number)
                hdr[3] = 0;
                hdr[4] = 8;  // 8 (offset to first IFD)
                hdr[5] = 0;
                hdr[6] = 0;
                hdr[7] = 0;
            }
            else
            {
                hdr[0] = 77; // "MM" (Motorola byte order)
                hdr[1] = 77;
                hdr[2] = 0;  // 42 (magic number)
                hdr[3] = 42;
                hdr[4] = 0;  // 8 (offset to first IFD)
                hdr[5] = 0;
                hdr[6] = 0;
                hdr[7] = 8;
            }
	        ot.Write(hdr);
        }
	
        /** Writes one 12-byte IFD entry. */
        void writeEntry(BinaryWriter ot, int tag, int fieldType, int count, int value) 
        {
            writeShort(ot, tag);
            writeShort(ot, fieldType);
            writeInt(ot, count);
		        if (count==1 && fieldType==TiffDecoder.SHORT) {
                writeShort(ot, value);
                writeShort(ot, 0);
            } else
			        writeInt(ot, value); // may be an offset
        }

        /** Writes one IFD (Image File Directory). */
        void writeIFD(BinaryWriter ot, int imageOffset, int nextIFD)
        {	
		        int tagDataOffset = HDR_SIZE + ifdSize;
            writeShort(ot, nEntries);
            writeEntry(ot, TiffDecoder.NEW_SUBFILE_TYPE, 4, 1, 0);
            writeEntry(ot, TiffDecoder.IMAGE_WIDTH, 4, 1, fi.width);
            writeEntry(ot, TiffDecoder.IMAGE_LENGTH, 4, 1, fi.height);
		    if (fi.fileType==FileInfo.RGB||fi.fileType==FileInfo.RGB48) {
                writeEntry(ot, TiffDecoder.BITS_PER_SAMPLE, 3, 3, tagDataOffset);
                tagDataOffset += BPS_DATA_SIZE;
            } else
			    writeEntry(ot, TiffDecoder.BITS_PER_SAMPLE,  3, 1, bitsPerSample);
            writeEntry(ot, TiffDecoder.COMPRESSION,  3, 1, 1); //No Compression
            writeEntry(ot, TiffDecoder.PHOTO_INTERP, 3, 1, photoInterp);
		    if (description!=null) {
                writeEntry(ot, TiffDecoder.IMAGE_DESCRIPTION, 2, description.Length, tagDataOffset);
                tagDataOffset += description.Length;
            }
            writeEntry(ot, TiffDecoder.STRIP_OFFSETS,    4, 1, imageOffset);
            writeEntry(ot, TiffDecoder.SAMPLES_PER_PIXEL,3, 1, samplesPerPixel);
            writeEntry(ot, TiffDecoder.ROWS_PER_STRIP,   4, 1, fi.height);
            writeEntry(ot, TiffDecoder.STRIP_BYTE_COUNT, 4, 1, imageSize);
		    if (fi.unit!=null && fi.pixelWidth!=0 && fi.pixelHeight!=0) {
                writeEntry(ot, TiffDecoder.X_RESOLUTION, 5, 1, tagDataOffset);
                writeEntry(ot, TiffDecoder.Y_RESOLUTION, 5, 1, tagDataOffset+8);
                tagDataOffset += SCALE_DATA_SIZE;
                int unit = 1;
                if (fi.unit.Equals("inch"))
                    unit = 2;
                else if (fi.unit.Equals("cm"))
                    unit = 3;
                writeEntry(ot, TiffDecoder.RESOLUTION_UNIT, 3, 1, unit);
            }
		    if (fi.fileType==FileInfo.GRAY32_FLOAT) {
                int format = TiffDecoder.FLOATING_POINT;
                writeEntry(ot, TiffDecoder.SAMPLE_FORMAT, 3, 1, format);
            }
		    if (colorMapSize>0) {
                writeEntry(ot, TiffDecoder.COLOR_MAP, 3, MAP_SIZE, tagDataOffset);
                tagDataOffset += MAP_SIZE*2;
            }
		    if (metaDataSize>0) {
                writeEntry(ot, TiffDecoder.META_DATA_BYTE_COUNTS, 4, nMetaDataEntries, tagDataOffset);
                writeEntry(ot, TiffDecoder.META_DATA, 1, metaDataSize, tagDataOffset+4*nMetaDataEntries);
                tagDataOffset += nMetaDataEntries*4 + metaDataSize;
            }
            writeInt(ot, nextIFD);
        }

        /** Writes the 6 bytes of data required by RGB BitsPerSample tag. */
        void writeBitsPerPixel(BinaryWriter ot)
        {
		        int bitsPerPixel = fi.fileType==FileInfo.RGB48?16:8;
            writeShort(ot, bitsPerPixel);
            writeShort(ot, bitsPerPixel);
            writeShort(ot, bitsPerPixel);
        }

        /** Writes the 16 bytes of data required by the XResolution and YResolution tags. */
        void writeScale(BinaryWriter ot)
        {
		        double xscale = 1.0/fi.pixelWidth;
		        double yscale = 1.0/fi.pixelHeight;
		        double scale = 1000000.0;
		        if (xscale*scale>int.MaxValue||yscale*scale>int.MaxValue)
			        scale = (int)(int.MaxValue/Math.Max(xscale,yscale));
            writeInt(ot, (int)(xscale*scale));
            writeInt(ot, (int)scale);
            writeInt(ot, (int)(yscale*scale));
            writeInt(ot, (int)scale);
        }

        /** Writes the variable length ImageDescription string. */
        void writeDescription(BinaryWriter ot)
        {
		    ot.Write(description,0,description.Length);
        }

        /** Writes color palette following the image. */
        void writeColorMap(BinaryWriter ot)
        {
		    byte[]
            colorTable16 = new byte[MAP_SIZE*2];
            int j = littleEndian ? 1 : 0;
            for (int i = 0; i<fi.lutSize; i++)
            {
                colorTable16[j] = fi.reds[i];
                colorTable16[512+j] = fi.greens[i];
                colorTable16[1024+j] = fi.blues[i];
                j += 2;
            }
		    ot.Write(colorTable16);
	    }   
	
	    /** Writes image metadata ("info" property, 
		    stack slice labels, channel display ranges, luts, ROIs,
		    overlays, properties and extra metadata). */
        void writeMetaData(BinaryWriter ot)
        {

            // write byte counts (META_DATA_BYTE_COUNTS tag)
            writeInt(ot, 4+nMetaDataTypes*8); // header size	
		    if (fi.info!=null && fi.info.Length>0)
			    writeInt(ot, fi.info.Length*2);
		    for (int i=0; i<nSliceLabels; i++) {
                if (fi.sliceLabels[i]==null)
                    writeInt(ot, 0);
                else
                    writeInt(ot, fi.sliceLabels[i].Length*2);
            }
		    if (fi.displayRanges!=null)
			    writeInt(ot, fi.displayRanges.Length*8);
		    if (fi.channelLuts!=null) {
                for (int i = 0; i<fi.channelLuts.Length; i++)
                    writeInt(ot, fi.channelLuts[i].Length);
            }
		        if (fi.plot!=null)
			        writeInt(ot, fi.plot.Length);
		        if (fi.roi!=null)
			        writeInt(ot, fi.roi.Length);
		        if (fi.overlay!=null) {
                for (int i = 0; i<fi.overlay.Length; i++)
                    writeInt(ot, fi.overlay[i].Length);
            }
		        if (fi.properties!=null) {
                for (int i = 0; i<fi.properties.Length; i++)
                    writeInt(ot, fi.properties[i].Length*2);
            }
		        for (int i=0; i<extraMetaDataEntries; i++)
			        writeInt(ot, fi.metaData [i].Length);

            // write header (META_DATA tag header)
            writeInt(ot, TiffDecoder.MAGIC_NUMBER); // "IJIJ"
		        if (fi.info!=null) {
                writeInt(ot, TiffDecoder.INFO); // type="info"
                writeInt(ot, 1); // count
            }
		        if (nSliceLabels>0) {
                writeInt(ot, TiffDecoder.LABELS); // type="labl"
                writeInt(ot, nSliceLabels); // count
            }
		        if (fi.displayRanges!=null) {
                writeInt(ot, TiffDecoder.RANGES); // type="rang"
                writeInt(ot, 1); // count
            }
		        if (fi.channelLuts!=null) {
                writeInt(ot, TiffDecoder.LUTS); // type="luts"
                writeInt(ot, fi.channelLuts.Length); // count
            }
		        if (fi.plot!=null) {
                writeInt(ot, TiffDecoder.PLOT); // type="plot"
                writeInt(ot, 1); // count
            }
		        if (fi.roi!=null) {
                writeInt(ot, TiffDecoder.ROI); // type="roi "
                writeInt(ot, 1); // count
            }
		        if (fi.overlay!=null) {
                writeInt(ot, TiffDecoder.OVERLAY); // type="over"
                writeInt(ot, fi.overlay.Length); // count
            }
		        if (fi.properties!=null) {
                writeInt(ot, TiffDecoder.PROPERTIES); // type="prop"
                writeInt(ot, fi.properties.Length); // count
            }
		        for (int i=0; i<extraMetaDataEntries; i++) {
                writeInt(ot, fi.metaDataTypes[i]);
                writeInt(ot, 1); // count
            }
		        
		        // write data (META_DATA tag body)
		        if (fi.info!=null)
			        writeChars(ot, fi.info);
		        for (int i=0; i<nSliceLabels; i++) {
                if (fi.sliceLabels[i]!=null)
                    writeChars(ot, fi.sliceLabels[i]);
            }
		        if (fi.displayRanges!=null) {
                for (int i = 0; i<fi.displayRanges.Length; i++)
                    writeDouble(ot, fi.displayRanges[i]);
            }
		        if (fi.channelLuts!=null) {
                for (int i = 0; i<fi.channelLuts.Length; i++)
				        ot.Write(fi.channelLuts[i]);
            }
		        if (fi.plot!=null)
			        ot.Write(fi.plot);
		        if (fi.roi!=null)
			        ot.Write(fi.roi);
		        if (fi.overlay!=null) {
                for (int i = 0; i<fi.overlay.Length; i++)
				        ot.Write(fi.overlay[i]);
            }
		        if (fi.properties!=null) {
                for (int i = 0; i<fi.properties.Length; i++)
                    writeChars(ot, fi.properties[i]);
            }
		        for (int i=0; i<extraMetaDataEntries; i++)
			        ot.Write(fi.metaData [i]);
        }

        /** Creates an optional image description string for saving calibration data.
            For stacks, also saves the stack size so ImageJ can open the stack without
            decoding an IFD for each slice.*/
        void makeDescriptionString()
        {
            if (fi.description!=null)
            {
                if (fi.description[fi.description.Length-1]!=(char)0)
                    fi.description += " ";
                description = Encoding.UTF8.GetBytes(fi.description);
                description[description.Length-1] = (byte)0;
            }
            else
                description = null;
        }

        void writeShort(BinaryWriter ot, int v) 
        {
		    if (littleEndian) {
       		    ot.Write(v&255);
        	    ot.Write((v>>8)&255);
            } else {
        	    ot.Write((v>>8)&255);
        	    ot.Write(v&255);
            }
        }

        void writeInt(BinaryWriter ot, int v)
        {
		    if (littleEndian) {
        	    ot.Write(v&255);
        	    ot.Write((v>>8)&255);
        	    ot.Write((v>>16)&255);
         	    ot.Write((v>>24)&255);
            } else {
        	    ot.Write((v>>24)&255);
        	    ot.Write((v>>16)&255);
        	    ot.Write((v>>8)&255);
        	    ot.Write(v&255);
            }
        }

        void writeLong(BinaryWriter ot, long v) 
        {
    	    if (littleEndian) {
                buffer[7] = (byte)(v>>56);
                buffer[6] = (byte)(v>>48);
                buffer[5] = (byte)(v>>40);
                buffer[4] = (byte)(v>>32);
                buffer[3] = (byte)(v>>24);
                buffer[2] = (byte)(v>>16);
                buffer[1] = (byte)(v>> 8);
                buffer[0] = (byte)v;
			    ot.Write(buffer, 0, 8);
            } else {
                buffer[0] = (byte)(v>>56);
                buffer[1] = (byte)(v>>48);
                buffer[2] = (byte)(v>>40);
                buffer[3] = (byte)(v>>32);
                buffer[4] = (byte)(v>>24);
                buffer[5] = (byte)(v>>16);
                buffer[6] = (byte)(v>> 8);
                buffer[7] = (byte)v;
			    ot.Write(buffer, 0, 8);
            }
        }

        void writeDouble(BinaryWriter ot, double v)
        {
            writeLong(ot, v.doubleToLongBits());
        }

        void writeChars(BinaryWriter ot, String s)
        {
                int len = s.Length;
                if (littleEndian) {
                    for (int i = 0; i < len; i++)
                    {
                        int v = s[i];
				            ot.Write(v&255); 
				            ot.Write((v>>8)&255);
                    }
                } else {
                    for (int i = 0; i < len; i++)
                    {
                        int v = s[i];
				            ot.Write((v>>8)&255); 
				            ot.Write(v&255);
                    }
                }
        }
    
}

}
